package org.test4j.mock.processor.filer.file;

import com.squareup.javapoet.*;
import g_asm.org.objectweb.asm.Type;
import lombok.Getter;
import org.test4j.mock.faking.fluent.FluentMockUp;
import org.test4j.mock.faking.fluent.MethodBehaviors;
import org.test4j.mock.faking.fluent.MockMethodWithTimes;
import org.test4j.mock.faking.meta.MethodId;
import org.test4j.mock.faking.util.TypeUtility;
import org.test4j.mock.processor.MockProcessor;
import org.test4j.mock.processor.filer.ClassNames;

import javax.lang.model.element.Modifier;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.lang.reflect.Modifier.isAbstract;
import static java.lang.reflect.Modifier.isNative;
import static org.test4j.mock.faking.util.AsmConstant.*;
import static org.test4j.mock.processor.filer.ClassNames.MockUp_Suffix;
import static org.test4j.mock.processor.filer.ClassNames.Mock_Package_WithDot;

public abstract class MockUpFiler {
    private static final String SUFFIX_SUPER = "$";

    protected ClassName className;
    /**
     * 在本类中声明实现的方法
     */
    protected List<MethodId> declaredMethodIds = new ArrayList<>();
    /**
     * 类或接口中声明的未实现的抽象方法
     */
    protected List<MethodId> abstractMethodIds = new ArrayList<>();

    /**
     * 已经添加到declaredMethodIds + abstractMethodIds集合中的方法
     */
    private Set<String> existNames = new HashSet<>();
    /**
     * 检测到的同名方法列表
     */
    private Set<String> sameNames = new HashSet<>();

    private String classDesc;
    /**
     * simple name: full name
     */
    @Getter
    protected String superClass;

    public MockUpFiler(ClassName className) {
        this.className = className;
        this.classDesc = TypeUtility.classPath(className.reflectionName());
    }

    protected abstract void parserSuperClass();

    protected abstract void parseMethodIds();

    protected MethodId addMethodId(int access, String name, String descriptor) {
        if (Objects.equals(Method_CL_INIT, name) || isNative(access)) {
            return null;
        }
        MethodId methodId = new MethodId(this.classDesc, this.superClass, name, descriptor);
        if (declaredMethodIds.contains(methodId) || abstractMethodIds.contains(methodId)) {
            return null;
        }
        if (isAbstract(access)) {
            abstractMethodIds.add(methodId);
        } else {
            declaredMethodIds.add(methodId);
        }
        if (existNames.contains(name)) {
            sameNames.add(name);
        } else {
            existNames.add(name);
        }
        return methodId;
    }

    public void writeFiler() {
        String fullName = Mock_Package_WithDot + fullName(this.className) + MockUp_Suffix;
        ClassName mockUp = ClassNames.getClassName(fullName);
        TypeSpec.Builder builder = TypeSpec
            .classBuilder(mockUp.simpleName())
            .superclass(ClassName.get(FluentMockUp.class))
            .addModifiers(Modifier.PUBLIC);
        this.build(builder);
        JavaFile.Builder javaBuilder = JavaFile.builder(mockUp.packageName(), builder.build());
        MockProcessor.writeFiler(javaBuilder.build());
    }

    /**
     * 处理嵌套定义的类, 用'.'代替'$'
     *
     * @param className
     * @return
     */
    private String fullName(ClassName className) {
        if (className.enclosingClassName() == null) {
            String pack = className.packageName();
            return pack.isEmpty() ? className.simpleName() : pack + "." + className.simpleName();
        } else {
            return fullName(className.enclosingClassName()) + "." + className.simpleName();
        }
    }

    private void build(TypeSpec.Builder builder) {
        builder.addMethod(this.m_constructor1());
        builder.addMethod(this.m_constructor3());
        for (MethodId meta : this.declaredMethodIds) {
            builder.addField(this.f_method(meta, false));
        }
        for (MethodId meta : this.abstractMethodIds) {
            builder.addField(this.f_method(meta, true));
        }
        if (this.superClass != null) {
            builder.addField(this.f_superMock("super" + SUFFIX_SUPER, this.superClass));
        }
    }

    private MethodSpec m_constructor3() {
        return MethodSpec.constructorBuilder()
            .addParameter(Class.class, "declaredClass")
            .addParameter(MethodBehaviors.class, "behaviors")
            .addParameter(ParameterizedTypeName.get(Set.class, Integer.class), "fakedHashCodes")
            .addStatement("super(declaredClass, behaviors, fakedHashCodes)")
            .addModifiers(Modifier.PUBLIC).build();
    }

    private MethodSpec m_constructor1() {
        return MethodSpec.constructorBuilder()
            .addModifiers(Modifier.PUBLIC)
            .addParameter(ArrayTypeName.of(Object.class), "targets")
            .varargs(true)
            .addStatement("super($S, targets)", this.className.reflectionName())
            .build();
    }

    private FieldSpec f_superMock(String name, String clazz) {
        ClassName type = ClassNames.getClassName(Mock_Package_WithDot + clazz + MockUp_Suffix);
        return FieldSpec.builder(type, name, Modifier.PUBLIC, Modifier.FINAL)
            .initializer("new $T(this.declaredToFake, this.behaviors, super.fakedHashCodes)", type)
            .build();
    }

    private FieldSpec f_method(MethodId meta, boolean isAbstract) {
        String fieldName = this.buildMethodMockField(meta.name, meta.getParams());
        if (isAbstract) {
            fieldName = fieldName + SUFFIX_SUPER;
        }
        TypeName type = ParameterizedTypeName.get(ClassName.get(MockMethodWithTimes.class), ClassNames.getClassName(meta.getReturnType()));
        FieldSpec.Builder builder = FieldSpec.builder(type, fieldName, Modifier.PUBLIC, Modifier.FINAL)
            .initializer("new MockMethodWithTimes<>(this,$S, $S, $S)", isAbstract ? null : meta.realClassDesc, meta.name, meta.desc)
            .addJavadoc("mock {@link $T#$L}", this.className, this.linkJavaDoc(meta));
        Map<Integer, MethodId.ParaNameType> params = meta.getParameters();
        if (params == null) {
            return builder.build();
        }
        builder.addJavadoc("\n<pre>");
        for (int index = 0; index < params.size(); index++) {
            if (!params.containsKey(index)) {
                continue;
            }
            String pName = params.get(index).name;
            String pType = this.getSimpleName(params.get(index));

            builder.addJavadoc("\n\t$L $L = inv.arg($L);", pType, pName, index);
        }
        builder.addJavadoc("\n</pre>");
        return builder.build();
    }

    private String getSimpleName(MethodId.ParaNameType nameType) {
        String pType = Type.getType(nameType.type).getClassName();
        int index = pType.lastIndexOf('.');
        return index >= 0 ? pType.substring(index + 1) : pType;
    }

    private String linkJavaDoc(MethodId meta) {
        StringBuffer buff = new StringBuffer();
        String name = meta.name;
        buff.append(Method_Init.equals(name) ? this.className.simpleName() : name).append("(");
        String paras = Stream.of(meta.getParams())
            .map(Type::getClassName)
            .collect(Collectors.joining(", "));
        buff.append(paras);
        return buff.append(")").toString();
    }

    /**
     * 构造mock方法引导字段名称
     * o 无同名方法，直接使用方法名称
     * o 有同名方法，使用方法名称+参数类型
     *
     * @param name
     * @param params
     * @return
     */
    private String buildMethodMockField(String name, Type[] params) {
        StringBuffer buff = new StringBuffer();
        if (Objects.equals(Method_Init, name)) {
            buff.append(Method_$Init);
        } else {
            buff.append(name);
        }
        if (!sameNames.contains(name)) {
            return buff.toString();
        }
        for (Type para : params) {
            String clazzName = TypeUtility.getShortClassName(para.getClassName());
            int index = clazzName.indexOf("[]");
            if (index > 0) {
                int size = (clazzName.length() - index) / 2;
                clazzName = clazzName.substring(0, index) + "D" + size;
            }
            buff.append("_").append(clazzName);
        }
        return buff.toString();
    }
}